 /*******************************************************************************
  * Copyright (c) 2005, 2006 IBM Corporation and others.
  * All rights reserved. This program and the accompanying materials
  * are made available under the terms of the Eclipse Public License v1.0
  * which accompanies this distribution, and is available at
  * http://www.eclipse.org/legal/epl-v10.html
  *
  * Contributors:
  * IBM Corporation - initial API and implementation
  *******************************************************************************/

 package org.eclipse.core.runtime.internal.adaptor;

 import java.io.*;
 import java.net.URL ;
 import java.util.*;
 import org.eclipse.core.runtime.adaptor.LocationManager;
 import org.eclipse.osgi.baseadaptor.*;
 import org.eclipse.osgi.baseadaptor.hooks.StorageHook;
 import org.eclipse.osgi.framework.adaptor.FrameworkAdaptor;
 import org.eclipse.osgi.framework.internal.core.Constants;
 import org.eclipse.osgi.framework.internal.core.FrameworkProperties;
 import org.eclipse.osgi.framework.log.FrameworkLogEntry;
 import org.eclipse.osgi.framework.util.Headers;
 import org.eclipse.osgi.framework.util.KeyedElement;
 import org.eclipse.osgi.internal.baseadaptor.AdaptorUtil;
 import org.eclipse.osgi.service.datalocation.Location;
 import org.eclipse.osgi.service.pluginconversion.PluginConversionException;
 import org.eclipse.osgi.util.ManifestElement;
 import org.eclipse.osgi.util.NLS;
 import org.osgi.framework.BundleException;
 import org.osgi.framework.Version;

 public final class EclipseStorageHook implements StorageHook, HookConfigurator {
     // System property used to check timestamps of the bundles in the configuration
 private static final String PROP_CHECK_CONFIG = "osgi.checkConfiguration"; //$NON-NLS-1$
 private static final String PROP_COMPATIBILITY_LAZYSTART = "osgi.compatibility.eagerStart.LazyActivation"; //$NON-NLS-1$
 private static final boolean COMPATIBILITY_LAZYSTART = Boolean.valueOf(FrameworkProperties.getProperty(PROP_COMPATIBILITY_LAZYSTART, "true")).booleanValue(); //$NON-NLS-1$
 private static final int STORAGE_VERION = 2;

     public static final String KEY = EclipseStorageHook.class.getName();
     public static final int HASHCODE = KEY.hashCode();

     private static final byte FLAG_LAZY_START = 0x01;
     private static final byte FLAG_HAS_PACKAGE_INFO = 0x02;
     // Note that the 0x04 was used in previous versions, if a new flag is needed then do not reuse this one
 //private static final byte FLAG_ACTIVATE_ON_CLASSLOAD = 0x04;
 // Flag to indicate that an include directive is present on the lazy activation policy
 private static final byte FLAG_HAS_LAZY_INCLUDE = 0x08;

     /** data to detect modification made in the manifest */
     private long manifestTimeStamp = 0;
     private byte manifestType = PluginConverterImpl.MANIFEST_TYPE_UNKNOWN;

     private BaseData bundledata;

     /** the Plugin-Class header */
     private String pluginClass = null;
     /** Eclipse-LazyStart header */
     private String [] lazyStartExcludes;
     private String [] lazyStartIncludes;
     /** shortcut to know if a bundle has a buddy */
     private String buddyList;
     /** shortcut to know if a bundle is a registrant to a registered policy */
     private String registeredBuddyList;
     private byte flags = 0;

     public int getStorageVersion() {
         return STORAGE_VERION;
     }

     public StorageHook create(BaseData bundledata) throws BundleException {
         EclipseStorageHook storageHook = new EclipseStorageHook();
         storageHook.bundledata = bundledata;
         return storageHook;
     }

     public void initialize(Dictionary manifest) throws BundleException {
         String activationPolicy = (String ) manifest.get(Constants.BUNDLE_ACTIVATIONPOLICY);
         if (activationPolicy != null) {
             parseActivationPolicy(this, activationPolicy);
         } else {
             String lazyStart = (String ) manifest.get(Constants.ECLIPSE_LAZYSTART);
             if (lazyStart == null)
                 lazyStart = (String ) manifest.get(Constants.ECLIPSE_AUTOSTART);
             parseLazyStart(this, lazyStart);
         }
         pluginClass = (String ) manifest.get(Constants.PLUGIN_CLASS);
         buddyList = (String ) manifest.get(Constants.BUDDY_LOADER);
         registeredBuddyList = (String ) manifest.get(Constants.REGISTERED_POLICY);
         if (hasPackageInfo(bundledata.getEntry(Constants.OSGI_BUNDLE_MANIFEST)))
             flags |= FLAG_HAS_PACKAGE_INFO;
         String genFrom = (String ) manifest.get(PluginConverterImpl.GENERATED_FROM);
         if (genFrom != null) {
             ManifestElement generatedFrom = ManifestElement.parseHeader(PluginConverterImpl.GENERATED_FROM, genFrom)[0];
             if (generatedFrom != null) {
                 manifestTimeStamp = Long.parseLong(generatedFrom.getValue());
                 manifestType = Byte.parseByte(generatedFrom.getAttribute(PluginConverterImpl.MANIFEST_TYPE_ATTRIBUTE));
             }
         }
         if (isAutoStartable()) {
             bundledata.setStatus(bundledata.getStatus() | Constants.BUNDLE_LAZY_START);
             if (COMPATIBILITY_LAZYSTART)
                 bundledata.setStatus(bundledata.getStatus() | Constants.BUNDLE_STARTED | Constants.BUNDLE_ACTIVATION_POLICY);
         }
     }

     public StorageHook load(BaseData target, DataInputStream in) throws IOException {
         EclipseStorageHook storageHook = new EclipseStorageHook();
         storageHook.bundledata = target;
         storageHook.flags = in.readByte();
         int pkgCount = in.readInt();
         String [] packageList = pkgCount > 0 ? new String [pkgCount] : null;
         for (int i = 0; i < pkgCount; i++)
             packageList[i] = in.readUTF();
         storageHook.lazyStartExcludes = packageList;
         if ((storageHook.flags & FLAG_HAS_LAZY_INCLUDE) != 0) {
             pkgCount = in.readInt();
             packageList = pkgCount > 0 ? new String [pkgCount] : null;
             for (int i = 0; i < pkgCount; i++)
                 packageList[i] = in.readUTF();
             storageHook.lazyStartIncludes = packageList;
         }
         storageHook.buddyList = AdaptorUtil.readString(in, false);
         storageHook.registeredBuddyList = AdaptorUtil.readString(in, false);
         storageHook.pluginClass = AdaptorUtil.readString(in, false);
         storageHook.manifestTimeStamp = in.readLong();
         storageHook.manifestType = in.readByte();
         if (storageHook.isAutoStartable()) {
             if ((target.getStatus() & Constants.BUNDLE_LAZY_START) == 0)
                 target.setStatus(target.getStatus() | Constants.BUNDLE_LAZY_START);
             // if the compatibility flag is set then we must make sure the persistent start bit is set and the activation policy bit;
 // if the persistent start bit was already set then we should not set the activation policy bit because this is an "eager" started bundle.
 if (COMPATIBILITY_LAZYSTART && (target.getStatus() & Constants.BUNDLE_STARTED ) == 0)
                 target.setStatus(target.getStatus() | Constants.BUNDLE_STARTED | Constants.BUNDLE_ACTIVATION_POLICY);
         }
         return storageHook;
     }

     public void save(DataOutputStream out) throws IOException {
         if (bundledata == null)
             throw new IllegalStateException ();
         // when this is stored back we always use the has include/exclude flag
 out.writeByte(flags);
         String [] excludes = getLazyStartExcludes();
         if (excludes == null)
             out.writeInt(0);
         else {
             out.writeInt(excludes.length);
             for (int i = 0; i < excludes.length; i++)
                 out.writeUTF(excludes[i]);
         }
         if ((flags & FLAG_HAS_LAZY_INCLUDE) != 0) {
             String [] includes = getLazyStartIncludes();
             if (includes == null)
                 out.writeInt(0);
             else {
                 out.writeInt(includes.length);
                 for (int i = 0; i < includes.length; i++)
                     out.writeUTF(includes[i]);
             }
         }
         AdaptorUtil.writeStringOrNull(out, getBuddyList());
         AdaptorUtil.writeStringOrNull(out, getRegisteredBuddyList());
         AdaptorUtil.writeStringOrNull(out, getPluginClass());
         out.writeLong(getManifestTimeStamp());
         out.writeByte(getManifestType());
     }

     public int getKeyHashCode() {
         return HASHCODE;
     }

     public boolean compare(KeyedElement other) {
         return other.getKey() == KEY;
     }

     public Object getKey() {
         return KEY;
     }

     public boolean isLazyStart() {
         return (flags & FLAG_LAZY_START) == FLAG_LAZY_START;
     }

     public String [] getLazyStartExcludes() {
         return lazyStartExcludes;
     }

     public String [] getLazyStartIncludes() {
         return lazyStartIncludes;
     }

     public String getBuddyList() {
         return buddyList;
     }

     public boolean hasPackageInfo() {
         return (flags & FLAG_HAS_PACKAGE_INFO) == FLAG_HAS_PACKAGE_INFO;
     }

     public String getPluginClass() {
         return pluginClass;
     }

     public String getRegisteredBuddyList() {
         return registeredBuddyList;
     }

     public long getManifestTimeStamp() {
         return manifestTimeStamp;
     }

     public byte getManifestType() {
         return manifestType;
     }

     /**
      * Checks whether this bundle is auto started for all resource/class loads or only for a
      * subset of resource/classloads
      * @return true if the bundle is auto started; false otherwise
      */
     public boolean isAutoStartable() {
         return isLazyStart() || (lazyStartExcludes != null && lazyStartExcludes.length > 0);
     }

     private void parseLazyStart(EclipseStorageHook storageHook, String headerValue) {
         storageHook.lazyStartExcludes = null;
         ManifestElement[] allElements = null;
         try {
             allElements = ManifestElement.parseHeader(Constants.ECLIPSE_LAZYSTART, headerValue);
         } catch (BundleException e) {
             // just use the default settings (no auto activation)
 String message = NLS.bind(EclipseAdaptorMsg.ECLIPSE_CLASSLOADER_CANNOT_GET_HEADERS, storageHook.bundledata.getLocation());
             bundledata.getAdaptor().getFrameworkLog().log(new FrameworkLogEntry(FrameworkAdaptor.FRAMEWORK_SYMBOLICNAME, FrameworkLogEntry.ERROR, 0, message, 0, e, null));
         }
         //Eclipse-AutoStart not found...
 if (allElements == null)
             return;
         // the single value for this element should be true|false
 if ("true".equalsIgnoreCase(allElements[0].getValue())) //$NON-NLS-1$
 storageHook.flags |= FLAG_LAZY_START;
         // look for any exceptions (the attribute) to the autoActivate setting
 String [] exceptions = ManifestElement.getArrayFromList(allElements[0].getAttribute(Constants.ECLIPSE_LAZYSTART_EXCEPTIONS));
         storageHook.lazyStartExcludes = exceptions;
     }

     private void parseActivationPolicy(EclipseStorageHook storageHook, String headerValue) {
         storageHook.lazyStartExcludes = null;
         ManifestElement[] allElements = null;
         try {
             allElements = ManifestElement.parseHeader(Constants.BUNDLE_ACTIVATIONPOLICY, headerValue);
         } catch (BundleException e) {
             // just use the default settings (no auto activation)
 String message = NLS.bind(EclipseAdaptorMsg.ECLIPSE_CLASSLOADER_CANNOT_GET_HEADERS, storageHook.bundledata.getLocation());
             bundledata.getAdaptor().getFrameworkLog().log(new FrameworkLogEntry(FrameworkAdaptor.FRAMEWORK_SYMBOLICNAME, FrameworkLogEntry.ERROR, 0, message, 0, e, null));
         }
         //Bundle-ActivationPolicy not found.
 if (allElements == null)
             return;
         // the single value for this type is lazy
 if (!Constants.ACTIVATION_LAZY.equalsIgnoreCase(allElements[0].getValue())) //$NON-NLS-1$
 return;
         storageHook.flags |= FLAG_LAZY_START;
         // look for any include or exclude attrs
 storageHook.lazyStartExcludes = ManifestElement.getArrayFromList(allElements[0].getDirective(Constants.EXCLUDE_DIRECTIVE));
         storageHook.lazyStartIncludes = ManifestElement.getArrayFromList(allElements[0].getDirective(Constants.INCLUDE_DIRECTIVE));
         if (storageHook.lazyStartIncludes != null)
             storageHook.flags |= FLAG_HAS_LAZY_INCLUDE;
     }

     // Used to check the bundle manifest file for any package information.
 // This is used when '.' is on the Bundle-ClassPath to prevent reading
 // the bundle manifest for pacakge information when loading classes.
 private static boolean hasPackageInfo(URL url) {
         if (url == null)
             return false;
         BufferedReader br = null;
         try {
             br = new BufferedReader(new InputStreamReader(url.openStream()));
             String line;
             while ((line = br.readLine()) != null) {
                 if (line.startsWith("Specification-Title: ") || line.startsWith("Specification-Version: ") || line.startsWith("Specification-Vendor: ") || line.startsWith("Implementation-Title: ") || line.startsWith("Implementation-Version: ") || line.startsWith("Implementation-Vendor: ")) //$NON-NLS-1$ //$NON-NLS-2$//$NON-NLS-3$ //$NON-NLS-4$ //$NON-NLS-5$ //$NON-NLS-6$
 return true;
             }
         } catch (IOException ioe) {
             // do nothing
 } finally {
             if (br != null)
                 try {
                     br.close();
                 } catch (IOException e) {
                     // do nothing
 }
         }
         return false;
     }

     public void addHooks(HookRegistry hookRegistry) {
         hookRegistry.addStorageHook(this);
     }

     private void checkTimeStamp() throws IllegalArgumentException {
         if (!checkManifestTimeStamp())
             throw new IllegalArgumentException ();
     }

     private boolean checkManifestTimeStamp() {
         if (!"true".equalsIgnoreCase(FrameworkProperties.getProperty(EclipseStorageHook.PROP_CHECK_CONFIG))) //$NON-NLS-1$
 return true;
         if (PluginConverterImpl.getTimeStamp(bundledata.getBundleFile().getBaseFile(), getManifestType()) == getManifestTimeStamp()) {
             if ((getManifestType() & (PluginConverterImpl.MANIFEST_TYPE_JAR | PluginConverterImpl.MANIFEST_TYPE_BUNDLE)) != 0)
                 return true;
             String cacheLocation = FrameworkProperties.getProperty(LocationManager.PROP_MANIFEST_CACHE);
             Location parentConfiguration = LocationManager.getConfigurationLocation().getParentLocation();
             if (parentConfiguration != null) {
                 try {
                     return checkManifestAndParent(cacheLocation, bundledata.getSymbolicName(), bundledata.getVersion().toString(), getManifestType()) != null;
                 } catch (BundleException e) {
                     return false;
                 }
             }
             File cacheFile = new File(cacheLocation, bundledata.getSymbolicName() + '_' + bundledata.getVersion() + ".MF"); //$NON-NLS-1$
 if (cacheFile.isFile())
                 return true;
         }
         return false;
     }

     private Headers checkManifestAndParent(String cacheLocation, String symbolicName, String version, byte inputType) throws BundleException {
         Headers result = basicCheckManifest(cacheLocation, symbolicName, version, inputType);
         if (result != null)
             return result;
         Location parentConfiguration = null;
         if ((parentConfiguration = LocationManager.getConfigurationLocation().getParentLocation()) != null)
             result = basicCheckManifest(new File(parentConfiguration.getURL().getFile(), FrameworkAdaptor.FRAMEWORK_SYMBOLICNAME + '/' + LocationManager.MANIFESTS_DIR).toString(), symbolicName, version, inputType);
         return result;
     }

     private Headers basicCheckManifest(String cacheLocation, String symbolicName, String version, byte inputType) throws BundleException {
         File currentFile = new File(cacheLocation, symbolicName + '_' + version + ".MF"); //$NON-NLS-1$
 if (PluginConverterImpl.upToDate(currentFile, bundledata.getBundleFile().getBaseFile(), inputType)) {
             try {
                 return Headers.parseManifest(new FileInputStream(currentFile));
             } catch (FileNotFoundException e) {
                 // do nothing.
 }
         }
         return null;
     }

     Dictionary createCachedManifest(boolean firstTime) throws BundleException {
         return firstTime ? getGeneratedManifest() : new CachedManifest(this);
     }

     public Dictionary getGeneratedManifest() throws BundleException {
         Dictionary builtIn = AdaptorUtil.loadManifestFrom(bundledata);
         if (builtIn != null) {
             // the bundle has a built-in manifest - we may not have to generate one
 if (!isComplete(builtIn)) {
                 Dictionary generatedManifest = generateManifest(builtIn);
                 if (generatedManifest != null)
                     return generatedManifest;
             }
             // the manifest is complete or we could not complete it - take it as it is
 manifestType = PluginConverterImpl.MANIFEST_TYPE_BUNDLE;
             if (bundledata.getBundleFile().getBaseFile().isFile()) {
                 manifestTimeStamp = bundledata.getBundleFile().getBaseFile().lastModified();
                 manifestType |= PluginConverterImpl.MANIFEST_TYPE_JAR;
             } else
                 manifestTimeStamp = bundledata.getBundleFile().getEntry(Constants.OSGI_BUNDLE_MANIFEST).getTime();
             return builtIn;
         }
         Dictionary result = generateManifest(null);
         if (result == null)
             throw new BundleException(NLS.bind(EclipseAdaptorMsg.ECLIPSE_DATA_MANIFEST_NOT_FOUND, bundledata.getLocation()));
         return result;
     }

     private Dictionary generateManifest(Dictionary builtIn) throws BundleException {
         String cacheLocation = FrameworkProperties.getProperty(LocationManager.PROP_MANIFEST_CACHE);
         if (bundledata.getSymbolicName() != null) {
             Headers existingHeaders = checkManifestAndParent(cacheLocation, bundledata.getSymbolicName(), bundledata.getVersion().toString(), manifestType);
             if (existingHeaders != null)
                 return existingHeaders;
         }

         PluginConverterImpl converter = PluginConverterImpl.getDefault();
         if (converter == null)
             converter = new PluginConverterImpl(bundledata.getAdaptor(), bundledata.getAdaptor().getContext());

         Dictionary generatedManifest;
         try {
             generatedManifest = converter.convertManifest(bundledata.getBundleFile().getBaseFile(), true, null, true, null);
         } catch (PluginConversionException pce) {
             String message = NLS.bind(EclipseAdaptorMsg.ECLIPSE_CONVERTER_ERROR_CONVERTING, bundledata.getBundleFile().getBaseFile());
             throw new BundleException(message, pce);
         }

         //Now we know the symbolicId and the version of the bundle, we check to see if don't have a manifest for it already
 Version version = Version.parseVersion((String ) generatedManifest.get(Constants.BUNDLE_VERSION));
         String symbolicName = ManifestElement.parseHeader(org.osgi.framework.Constants.BUNDLE_SYMBOLICNAME, (String ) generatedManifest.get(org.osgi.framework.Constants.BUNDLE_SYMBOLICNAME))[0].getValue();
         ManifestElement generatedFrom = ManifestElement.parseHeader(PluginConverterImpl.GENERATED_FROM, (String ) generatedManifest.get(PluginConverterImpl.GENERATED_FROM))[0];
         Headers existingHeaders = checkManifestAndParent(cacheLocation, symbolicName, version.toString(), Byte.parseByte(generatedFrom.getAttribute(PluginConverterImpl.MANIFEST_TYPE_ATTRIBUTE)));
         //We don't have a manifest.
 manifestTimeStamp = Long.parseLong(generatedFrom.getValue());
         manifestType = Byte.parseByte(generatedFrom.getAttribute(PluginConverterImpl.MANIFEST_TYPE_ATTRIBUTE));
         if (bundledata.getAdaptor().isReadOnly() || existingHeaders != null)
             return existingHeaders;

         //merge the original manifest with the generated one
 if (builtIn != null) {
             Enumeration keysEnum = builtIn.keys();
             while (keysEnum.hasMoreElements()) {
                 Object key = keysEnum.nextElement();
                 generatedManifest.put(key, builtIn.get(key));
             }
         }

         //write the generated manifest
 File bundleManifestLocation = new File(cacheLocation, symbolicName + '_' + version.toString() + ".MF"); //$NON-NLS-1$
 try {
             converter.writeManifest(bundleManifestLocation, generatedManifest, true);
         } catch (Exception e) {
             //TODO Need to log
 }
         return generatedManifest;

     }

     private boolean isComplete(Dictionary manifest) {
         // a manifest is complete if it has a Bundle-SymbolicName entry...
 if (manifest.get(org.osgi.framework.Constants.BUNDLE_SYMBOLICNAME) != null)
             return true;
         // ...or it does not have a plugin/fragment manifest where to get the other entries from
 return bundledata.getEntry(PluginConverterImpl.PLUGIN_MANIFEST) == null && bundledata.getEntry(PluginConverterImpl.FRAGMENT_MANIFEST) == null;
     }

     public BaseData getBaseData() {
         return bundledata;
     }

     public void copy(StorageHook storageHook) {
         // copy nothing all must be re-read from a manifest
 }

     public void validate() throws IllegalArgumentException {
         checkTimeStamp();
     }

     public FrameworkAdaptor getAdaptor() {
         if (bundledata != null)
             return bundledata.getAdaptor();
         return null;
     }

     public Dictionary getManifest(boolean firstLoad) throws BundleException {
         return createCachedManifest(firstLoad);
     }

     public boolean forgetStatusChange(int status) {
         return false;
     }

     public boolean forgetStartLevelChange(int startlevel) {
         return false;
     }

     public boolean matchDNChain(String pattern) {
         return false;
     }
 }

